//@author A0096759E

package storage;

import googlecal.GoogleCalendar;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Stack;
import java.util.UUID;

import applogger.AppLogger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Storage implements StorageInterface {

	private static final String MSG_EMPTY_STRING = "";
	private static final String CLEAR_COMMAND = "clr";
	private static final String ENCODING_UTF8 = "UTF-8";
	private static final String STRING_DOUBLE_NEWLINE = "\n\n";
	private static final String STRING_NEWLINE = "\n";
	
	private static final String MSG_NO_COLLISION = "No collision.";

	private static final String SUCCESS_READ_SAVED_EVENTS = "Successfully Read Saved Inputs";
	private static final String SUCCESS_ALL_EVENTS_CLEARED = "All events have been cleared from your Schedule!";
	private static final String SUCCESS_DELETED_EVENT = "The event \"%s\" has been removed successfully from your Schedule!";
	private static final String SUCCESS_ADDED_EVENT = "The event \"%s\" has been added successfully to your Schedule!";
	private static final String SUCCESS_SORTED_BY_TYPE = "Your Schedule has successfully been sorted by Type!";
	private static final String SUCCESS_SORTED_BY_ALPHABET = "Your Schedule has successfully been sorted in Alphabetical Order!";
	private static final String SUCCESS_SORTED_BY_TIME = "Your Schedule has successfully been sorted by Time!";

	private static final int START_OF_DD = 0;
	private static final int START_OF_MM = 2;
	private static final int START_OF_YYYY = 4;
	private static final int SHIFT_LEFT_BY_TWO = 100;
	private static final int SHIFT_LEFT_BY_FOUR = 10000;

	private static final String ERROR_MSG_READ_SCHEDULE_FAILED = "ERROR! File couldn't be read. Either file does not exist, or corrupted!";
	private static final String ERROR_MSG_SAVE_SCHEDULE_FAILED = "ERROR! Unable to save latest changes! Please Restart and try again!";
	private static final String ERROR_MSG_INVALID_INDEX = "ERROR! An invalid Index was input. Unable to perform Delete. Please try again!";
	private static final String ERROR_MSG_ADD_FAILED = "ERROR! Unable to add task!";
	private static final String ERROR_MSG_NOT_FOUND = "ERROR! This task has already been deleted!";

	private static final String TO_SAVE_EVENTS_START = "{'events': [\r\n\r\n";
	private static final String TO_STRING_EVENT_TEMPLATE = "%s. %s";
	private static final String TO_SAVE_EVENTS_SEPARATOR = ",\r\n";
	private static final String TO_SAVE_EVENTS_END = "\r\n\r\n]}";

	private ArrayList<ScheduleEvent> eventsList = new ArrayList<ScheduleEvent>();
	private Stack<UndoRedoStackEntry> undoStack = new Stack<UndoRedoStackEntry>();
	private Stack<UndoRedoStackEntry> redoStack = new Stack<UndoRedoStackEntry>();
	private String fileName = "LifeShelf.txt"; // For easy JUnit Testing

	/**
	 * Constructor for normal Storage
	 */
	public Storage() {
		this.readPreviousSavedEventsList();
		GoogleCalendar.initGoogleSync(this);
	}

	/**
	 * Constructor for Storage for testing
	 */
	public Storage(String fileName) {
		this.fileName = fileName;
		this.readPreviousSavedEventsList();
		GoogleCalendar.initGoogleSync(this);
	}

	// Mutators
	
	/**
	 * Takes in an event and adds it to the arraylist of ScheduleEvent's
	 * @param scheduleEvent - the event to add
	 * @return success message or error message if fail
	 */
	public String addEvent(ScheduleEvent scheduleEvent) {
		try {

			AppLogger.log("Check for collision.");
			DetectCollision taskCollision = new DetectCollision(eventsList, scheduleEvent);
			
			String collisionNameCheck = taskCollision.checkNameCollision();
			String collisionTimeCheck = taskCollision.checkTimeCollision();
			
			if (collisionNameCheck != MSG_EMPTY_STRING || collisionTimeCheck.equals(MSG_NO_COLLISION)) {

				if (scheduleEvent.getUID() == null) {
					UUID newUID = UUID.randomUUID();
					scheduleEvent.setUID(newUID.toString());
				}

				eventsList.add(scheduleEvent);
				sortByTime();
				this.saveEvents();

				if (collisionNameCheck != MSG_EMPTY_STRING) {
					return collisionNameCheck + STRING_NEWLINE 
							+ String.format(SUCCESS_ADDED_EVENT, scheduleEvent.getName());
				} else {
					return String.format(SUCCESS_ADDED_EVENT, scheduleEvent.getName());
				} 
			}
			
			if (!collisionTimeCheck.equals(MSG_NO_COLLISION)) {
				if (scheduleEvent.getUID() == null) {
					UUID newUID = UUID.randomUUID();
					scheduleEvent.setUID(newUID.toString());
				}
				AppLogger.log("Adding event into Storage");
				eventsList.add(scheduleEvent);
				sortByTime();
				this.saveEvents();
				return collisionTimeCheck  + STRING_NEWLINE 
						+ String.format(SUCCESS_ADDED_EVENT, scheduleEvent.getName());
			}
			
			return String.format(SUCCESS_ADDED_EVENT, scheduleEvent.getName());

		} catch (Exception e) {
			AppLogger.log(e.toString());
			return ERROR_MSG_ADD_FAILED;
		}
	}

	/**
	 * Takes in an index and tries to delete it from the arraylist of ScheduleEvent's
	 * @param eventIndex
	 * @return success message or error message if fail
	 */
	public String deleteEvent(int eventIndex) {
		AppLogger.log("Deleting event from storage");
		try {
			if (eventIndex <= eventsList.size()) {
				ScheduleEvent deletedEvent = eventsList.remove(eventIndex - 1);
				this.saveEvents();
				return String.format(SUCCESS_DELETED_EVENT,
						deletedEvent.getName());
			} else {
				return ERROR_MSG_INVALID_INDEX;
			}
		} catch (Exception e) {
			this.redoStack.pop();
			AppLogger.log(e.toString());
			return ERROR_MSG_NOT_FOUND;
		}
	}

	/**
	 * Takes in an index and tries to mark the event from the arraylist as done
	 * @param eventIndex
	 * @return success message or error message if fail
	 */
	public String markEventDone(int eventIndex) {
		AppLogger.log("Marking event as done");
		try {
			ScheduleEvent eventToMark = eventsList.get(eventIndex - 1);
			String markResponse;
			markResponse = eventToMark.setIsDone();
			this.saveEvents();
			return markResponse;
		} catch (Exception e) {
			AppLogger.log(e.toString());
			return ERROR_MSG_INVALID_INDEX;
		}
	}

	/**
	 * Takes in an index and tries to mark the event from the arraylist as NOT done
	 * @param eventIndex
	 * @return success message or error message if fail
	 */
	public String markEventUndone(int eventIndex) {
		AppLogger.log("Marking event as not done");
		try {
			ScheduleEvent eventToMark = eventsList.get(eventIndex - 1);
			String markResponse = eventToMark.setNotDone();
			this.saveEvents();
			return markResponse;
		} catch (Exception e) {
			AppLogger.log(e.toString());
			return ERROR_MSG_INVALID_INDEX;
		}
	}

	/**
	 * clears all the events from the arraylist
	 * @return success message or error message if fail
	 */
	public String clearAllEvents() {
		AppLogger.log("Clearing all events from the Storage");
		for (int index = eventsList.size() - 1; index >= 0; index--) {
			ScheduleEvent deletedEvent = eventsList.remove(index);
			undoStack.push(new UndoRedoStackEntry(CLEAR_COMMAND, deletedEvent));
		}
		this.saveEvents();
		return SUCCESS_ALL_EVENTS_CLEARED;
	}

	/**
	 * Sorts the arraylist of events by alphabet
	 * @return success message or error message if fail
	 */
	public String sortByAlphabet() {
		AppLogger.log("Sorting by alphabet");
		Collections.sort(eventsList, new Comparator<ScheduleEvent>() {
			public int compare(ScheduleEvent scheduleEvent1,
					ScheduleEvent scheduleEvent2) {
				String firstEventName = scheduleEvent1.getName();
				String secondEventName = scheduleEvent2.getName();
				return firstEventName.compareToIgnoreCase(secondEventName);
			}
		});
		this.saveEvents();
		return SUCCESS_SORTED_BY_ALPHABET;
	}

	/**
	 * Sorts the arraylist of events by Time
	 * @return success message or error message if fail
	 */
	public String sortByTime() {
		AppLogger.log("Sorting by Time -- requires sort by alpha and type");
		sortByAlphabet();
		sortByType();
		Collections.sort(eventsList, new Comparator<ScheduleEvent>() {
			public int compare(ScheduleEvent scheduleEvent1,
					ScheduleEvent scheduleEvent2) {
				if (convertDateToInteger(scheduleEvent1.getDate()) > convertDateToInteger(scheduleEvent2
						.getDate())) {
					return 1;
				} else if (convertDateToInteger(scheduleEvent1.getDate()) < convertDateToInteger(scheduleEvent2
						.getDate())) {
					return -1;
				} else if (scheduleEvent1.getType() == 1
						&& scheduleEvent2.getType() == 1) {
					return (compareTimings(scheduleEvent1.getEndTime(),
							scheduleEvent2.getEndTime()));
				} else if (scheduleEvent1.getType() == 1
						&& scheduleEvent2.getType() == 2) {
					return (compareTimings(scheduleEvent1.getEndTime(),
							scheduleEvent2.getStartTime()));
				} else if (scheduleEvent1.getType() == 2
						&& scheduleEvent2.getType() == 1) {
					return (compareTimings(scheduleEvent1.getStartTime(),
							scheduleEvent2.getEndTime()));
				} else if (scheduleEvent1.getType() == 2
						&& scheduleEvent2.getType() == 2) {
					return (compareTimings(scheduleEvent1.getStartTime(),
							scheduleEvent2.getStartTime()));
				}
				return 0;
			}

			private int compareTimings(String endTime, String endTime2) {
				int endTimeOne = Integer.parseInt(endTime);
				int endTimeTwo = Integer.parseInt(endTime2);
				return (endTimeOne - endTimeTwo);
			}

			private int convertDateToInteger(String date) {
				int day, month, year;
				day = Integer.parseInt(date.substring(START_OF_DD, START_OF_MM));
				month = Integer.parseInt(date.substring(START_OF_MM,
						START_OF_YYYY)) * SHIFT_LEFT_BY_TWO;
				year = Integer.parseInt(date.substring(START_OF_YYYY))
						* SHIFT_LEFT_BY_FOUR;
				return (day + month + year);
			}
		});
		this.saveEvents();
		return SUCCESS_SORTED_BY_TIME;
	}

	/**
	 * Sorts the arraylist of events by Type
	 * @return success message or error message if fail
	 */
	public String sortByType() {
		AppLogger.log("Sorting by Type");
		Collections.sort(eventsList, new Comparator<ScheduleEvent>() {
			public int compare(ScheduleEvent scheduleEvent1,
					ScheduleEvent scheduleEvent2) {
				return (scheduleEvent1.getType() - scheduleEvent2.getType());
			}
		});
		this.saveEvents();
		return SUCCESS_SORTED_BY_TYPE;
	}

	/**
	 * Saves the events into a text file
	 * @return success message or error message if fail
	 */
	public String saveEvents() {
		AppLogger.log("Saving Events to text file");
		try {
			int i;
			PrintWriter out = new PrintWriter(fileName, ENCODING_UTF8);
			out.write(TO_SAVE_EVENTS_START);
			if (eventsList.size() > 0) {
				for (i = 0; i < eventsList.size() - 1; i++) {
					out.write(eventsList.get(i).toSave());
					out.write(TO_SAVE_EVENTS_SEPARATOR);
				}
				out.write(eventsList.get(i).toSave());
				out.write(TO_SAVE_EVENTS_END);
				out.close();
			}
		} catch (IOException e) {
			System.out.println(ERROR_MSG_SAVE_SCHEDULE_FAILED);
		}
		return null;
	}

	// Assessors
	
	/**
	 * Returns the event at the input eventIndex
	 * @param eventIndex
	 * @return the ScheduleEvent at given index
	 */
	public ScheduleEvent getEvent(int eventIndex) {
		return eventsList.get(eventIndex);
	}

	/**
	 * Returns the size of the events arraylist
	 * @return size of events arraylist
	 */
	public int getSize() {
		return eventsList.size();
	}


	/**
	 * Returns the undostack from this Storage
	 * @return the undostack
	 */
	public Stack<UndoRedoStackEntry> getUndoStack() {
		return this.undoStack;
	}

	/**
	 * Returns the redostack from this Storage
	 * @return the redostack
	 */
	public Stack<UndoRedoStackEntry> getRedoStack() {
		return this.redoStack;
	}

	/**
	 * Returns the event index of a particular input scheduleEvent from this Storage
	 * @param scheduleEvent to check
	 * @return index of input scheduleEvent
	 */
	public int getEventIndex(ScheduleEvent scheduleEvent) {
		return eventsList.indexOf(scheduleEvent);
	}

	/**
	 * Returns a clone of this Storage
	 * @return clone of Storage
	 */
	public ArrayList<ScheduleEvent> getStorageClone() {
		ArrayList<ScheduleEvent> clone = new ArrayList<ScheduleEvent>();
		for (int i = 0; i < eventsList.size(); i++) {
			clone.add(eventsList.get(i));
		}
		return clone;
	}
	

	
	/**
	 * Returns a schedule event that is in the arraylist that has a matching googleUID as the one input
	 * @param googleUID
	 * @return event with the same googleUID that is in storage
	 */
	public ScheduleEvent getEventByGoogleID(String googleUID) {
		try {
			// Run 5 times to reduce the problem of overlapping problems
			for (int j = 0; j < 5; j++) {
				for (int i = 0; i < eventsList.size(); i++) {
					ScheduleEvent eventToGetGoogleID = eventsList.get(i);
					if (eventToGetGoogleID.getGoogleUID() != null
							&& eventToGetGoogleID.getGoogleUID().equals(
									googleUID)) {
						return eventsList.get(i);
					}
				}
			}
		} catch (Exception e) {
			AppLogger.log(e.toString());
		}
		return null;
	}

	// Other Supporting Methods
	
	/**
	 * Loads up the previosu saved storage from text file.
	 * @return success message or error message if fail
	 */
	private String readPreviousSavedEventsList() {
		final SavedEventsList savedEventsList;
		try {
			final GsonBuilder gsonBuilder = new GsonBuilder();
			gsonBuilder.registerTypeAdapter(SavedEventsList.class,
					new SavedEventsListDeserializer());
			final Gson gson = gsonBuilder.create();

			try (Reader reader = new InputStreamReader(new FileInputStream(
					fileName), ENCODING_UTF8)) {
				savedEventsList = gson.fromJson(reader, SavedEventsList.class);
			}
			for (int i = 0; i < savedEventsList.getSize(); i++) {
				eventsList.add(savedEventsList.get(i));
			}

			return SUCCESS_READ_SAVED_EVENTS;
		} catch (Exception noSuchFile) {
			return ERROR_MSG_READ_SCHEDULE_FAILED;
		}
	}

	/**
	 * Clears the redo stack
	 */
	public void clearRedoStack() {
		this.redoStack.clear();
	}

	@Override
	public String toString() {
		StringBuilder stringEventsList = new StringBuilder();
		int index = 0;
		for (index = 0; index < eventsList.size() - 1; index++) {
			stringEventsList.append(String.format(TO_STRING_EVENT_TEMPLATE,
					index + 1, eventsList.get(index).toString()));
			stringEventsList.append(STRING_DOUBLE_NEWLINE);
		}
		if (eventsList.size() >= 1) {
			stringEventsList.append(String.format(TO_STRING_EVENT_TEMPLATE,
					index + 1, eventsList.get(index).toString()));
		}
		return stringEventsList.toString();
	}

	//@author A0101200R
	
	/**
	 * Returns a string of events that are done or undone, depending on 
	 * the value of doneOrUndone that is passed into the method.
	 * 
	 * @param doneOrUndone
	 * @return stringEventsList.toString()  
	 */
	public String doneString(boolean doneOrUndone) {
		StringBuilder stringEventsList = new StringBuilder();
		int index = 0;
		boolean isDone = false;

		for (index = 0; index < eventsList.size() - 1; index++) {
			isDone = eventsList.get(index).getIsDone();
			if (isDone == doneOrUndone) {
				String eventString = String.format(TO_STRING_EVENT_TEMPLATE,
						index + 1, eventsList.get(index).toString());
		
				stringEventsList.append(eventString);
				stringEventsList.append(STRING_DOUBLE_NEWLINE);
			}
		}
		
		if (eventsList.size() >= 1) {
			index = eventsList.size() - 1;
			isDone = eventsList.get(index).getIsDone();
			if (isDone == doneOrUndone) {
				String eventString = String.format(TO_STRING_EVENT_TEMPLATE,
						index + 1, eventsList.get(index).toString());
				
				stringEventsList.append(eventString);
			}
		}
		return stringEventsList.toString();
	}

	/**
	 * Returns a string of the events, with only the names 
	 * of the events. The details of the events are omitted. 
	 * 
	 * @return stringEventsList.toString()
	 */
	public String toShortString() {
		StringBuilder stringEventsList = new StringBuilder();
		int index = 0;
		for (index = 0; index < eventsList.size() - 1; index++) {
			stringEventsList.append(String.format(TO_STRING_EVENT_TEMPLATE,
					index + 1, eventsList.get(index).onlyNameString()));
			stringEventsList.append(STRING_DOUBLE_NEWLINE);
		}
		if (eventsList.size() >= 1) {
			stringEventsList.append(String.format(TO_STRING_EVENT_TEMPLATE,
					index + 1, eventsList.get(index).onlyNameString()));
		}
		return stringEventsList.toString();
	}

	/**
	 * Returns a string of events that are done or undone, depending on the value of 
	 * doneOrUndone passed into the method. The details of the events are omitted.
	 * 
	 * @param doneOrUndone
	 * @return stringEventsList.toString()
	 */
	public String doneShortString(boolean doneOrUndone) {
		StringBuilder stringEventsList = new StringBuilder();
		int index = 0;
		boolean isDone = false;

		for (index = 0; index < eventsList.size() - 1; index++) {
			isDone = eventsList.get(index).getIsDone();
			if (isDone == doneOrUndone) {
				String eventString = String.format(TO_STRING_EVENT_TEMPLATE,
						index + 1, eventsList.get(index).onlyNameString());
				
				stringEventsList.append(eventString);
				stringEventsList.append(STRING_DOUBLE_NEWLINE);
			}
		}
		
		if (eventsList.size() >= 1) {
			index = eventsList.size() - 1;
			isDone = eventsList.get(index).getIsDone();
			if (isDone == doneOrUndone) {
				String eventString = String.format(TO_STRING_EVENT_TEMPLATE,
						index + 1, eventsList.get(index).onlyNameString());
				
				stringEventsList.append(eventString);
			}
		}
		
		return stringEventsList.toString();
	}
}
